package net.intellect.force;

import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.axis2.transport.http.HTTPConstants;

import com.sforce.soap.enterprise.ID;
import com.sforce.soap.enterprise.InvalidIdFault;
import com.sforce.soap.enterprise.LoginFault;
import com.sforce.soap.enterprise.LoginResult;
import com.sforce.soap.enterprise.QueryOptions;
import com.sforce.soap.enterprise.QueryResult;
import com.sforce.soap.enterprise.SaveResult;
import com.sforce.soap.enterprise.SessionHeader;
import com.sforce.soap.enterprise.SforceServiceStub;
import com.sforce.soap.enterprise.UnexpectedErrorFault;
import com.sforce.soap.enterprise.sobject.SObject;

/**
 * Only class witch interacts with the salesforce.com platform.
 * Defines methods to connect this application with the saleforce.com platform services.
 * @author Intellect International (TRF)
 */
public class ClientSalesforce {

	// required for login
	private static SforceServiceStub binding;
	private static boolean useCompression;
	private static String userName;
	private static String password;
	private static String token;
	private static ID userID;
	private static String serverUrl;
	private static LoginResult loginResult;
	private static SessionHeader mySessionHeader;

	// Required for the new session
	private static String sessionId;
	//private long nextLoginTime;
	//private int    sessionlength;
	
	// max size of sobjects can be executed on a single query
	private static QueryOptions queryOptions;
	private static final long timeOut = 60000;
	private static final int querySize = 500;
	private static final int dmlStatementSize = 100;
	
	public static final String HOST_PRODUCTION = "https://login.salesforce.com/services/Soap/c/18.0";
	protected static Logger logger = Logger.getLogger("ClientSalesforce");
	
	public ClientSalesforce() { } ;

	public static void setLoginValues(String aUserName, String aPassword, String aToken) {
		userName = aUserName;
		password = aPassword;
		token = aToken;
		logger.getLevel();
	}
	
	/**
	  * Signs the user into the salesforce.com platform and creates a new session
	  * @throws UnexpectedErrorFault
	  * @throws InvalidIdFault
	  * @throws LoginFault
	  * @throws RemoteException
	  * @throws Exception
	  */
	public static void login() throws UnexpectedErrorFault, InvalidIdFault,LoginFault, RemoteException, Exception {

		resetBindingStub();
		binding._getServiceClient().getOptions().setTo(new org.apache.axis2.addressing.EndpointReference(HOST_PRODUCTION));		

		loginResult = binding.login(userName,token!=null?password+token:password,null);
		//nextLoginTime = System.currentTimeMillis() + (sessionlength * timeOut);

		sessionId = loginResult.getSessionId();
		serverUrl = loginResult.getServerUrl();

		//Create a new session header object and set the
		//session id to that returned by the login
		mySessionHeader = new SessionHeader();
		mySessionHeader.setSessionId(loginResult.getSessionId());

		binding = new SforceServiceStub(loginResult.getServerUrl());
		setCompressionOnBinding();
		userID = loginResult.getUserId();
		logger.log(Level.INFO, "Log in sucessfull: " + loginResult.getUserInfo().getUserFullName());
		logger.log(Level.INFO, "Server URL: " + serverUrl);
	}
	
	/**
	 * Executes a query string where batch Size is specified
	 * @param queryString
	 * @param batchSize
	 * @return SObject[] - sObjects Array returned by salesforce. 
	 * @throws UnexpectedErrorFault
	 * @throws InvalidIdFault
	 * @throws LoginFault
	 * @throws RemoteException
	 * @throws Exception
	 */
	public static List<SObject> query(String queryString, int batchSize) throws UnexpectedErrorFault, InvalidIdFault, LoginFault, RemoteException, Exception{
		checkLogin();
		SObject[] records = null;
		List<SObject> toReturn = new ArrayList<SObject>();
		logger.log(Level.INFO, "Ready to execute: " + queryString);
		
		// sets query options
		queryOptions = new QueryOptions();
		queryOptions.setBatchSize(batchSize); // size of ???
		
		try {
			QueryResult qr = binding.query(queryString, mySessionHeader, queryOptions, null, null);
			logger.log(Level.INFO, "QUERY RESULT: " + qr.getSize());
			if (qr.getSize() == 0) return null;
			records = qr.getRecords();
			for (int i=0; i<records.length; i++) toReturn.add(records[i]);

		} catch (RemoteException e) {
			logger.log(Level.SEVERE, "ClientSalesforce.query/"+"Error: " + e);
		} catch (InvalidIdFault e) {
			logger.log(Level.SEVERE, "ClientSalesforce.query/"+"Error: " + e);
		} catch (UnexpectedErrorFault e) {
			logger.log(Level.SEVERE, "ClientSalesforce.query/"+"Error: " + e);
		} catch (NullPointerException e) {
			logger.log(Level.SEVERE, "ClientSalesforce.query/"+"Error: " + e);
		} catch (Exception e) {
			logger.log(Level.SEVERE, "ClientSalesforce.query/"+"Error: " + e);
		}

		return toReturn;
	}
	
	/**
	 * Executes a query string where batch Size is specified and a list of ids for the IN clause are received. 
	 * @param queryString
	 * @param batchSize
	 * @param ids
	 * @return SObject[] - sObjects Array returned by salesforce. 
	 * @throws UnexpectedErrorFault
	 * @throws InvalidIdFault
	 * @throws LoginFault
	 * @throws RemoteException
	 * @throws Exception
	 */
	public static List<SObject> query(String queryString, int batchSize, List<String> ids, String extraQueryString) throws UnexpectedErrorFault, InvalidIdFault, LoginFault, RemoteException, Exception{
		
		if (ids != null && ids.size() >0) {
			queryString += " (";
			for (int i = 0; i<ids.size(); i++) {
				if (i != 0) queryString += ", ";
				queryString += "'" + ids.get(i) + "'";
			}
			queryString += ") " + extraQueryString;
		}
		
		return query(queryString, batchSize);
	}

	/**
	 * Executes a query string where batch Size is NOT specified and a list of ids for the IN clause are received. 
	 * @param queryString
	 * @param ids
	 * @return SObject[] - sObjects Array returned by salesforce. 
	 * @throws UnexpectedErrorFault
	 * @throws InvalidIdFault
	 * @throws LoginFault
	 * @throws RemoteException
	 * @throws Exception
	 */
	public static List<SObject> query(String queryString, List<String> ids, String extraQueryString) throws UnexpectedErrorFault, InvalidIdFault, LoginFault, RemoteException, Exception{
		return query(queryString, querySize, ids, extraQueryString);
	}

	public static SObject queryLimitOne(String queryString) throws UnexpectedErrorFault, InvalidIdFault, LoginFault, RemoteException, Exception{
		SObject object = null;
		List<SObject> objects = query(queryString, querySize);
		if (objects.size() > 0) object  = objects.get(0);
		return object;
	}

	/**
	 * Executes a query string where batch Size is NOT specified
	 * @param queryString
	 * @return SObject[] - sObjects Array returned by salesforce.
	 * @throws UnexpectedErrorFault
	 * @throws InvalidIdFault
	 * @throws LoginFault
	 * @throws RemoteException
	 * @throws Exception
	 */
	public static List<SObject> query(String queryString) throws UnexpectedErrorFault, InvalidIdFault, LoginFault, RemoteException, Exception{
		return query(queryString, querySize);
	}

	/**
	 * Inserts new records into the database when batch size is specified
	 * @param records
	 * @param batchSize
	 * @return SaveResult Set of records returned by salesforce (each includes id)
	 * @throws UnexpectedErrorFault
	 * @throws InvalidIdFault
	 * @throws LoginFault
	 * @throws RemoteException
	 * @throws IllegalArgumentException
	 * @throws Exception
	 */
	public static List<SObject> create(List<SObject> records, int batchSize) throws UnexpectedErrorFault, InvalidIdFault,LoginFault, RemoteException, IllegalArgumentException, Exception {
		logger.log(Level.INFO, "Ready to insert: " + records.size() + " records.");
		List<SObject> returnList = new ArrayList<SObject>();
		SaveResult[] saveResults = null;
		boolean errorFound = false;
		String errors = new String();
		SObject[] objs = new SObject[records.size()];

		try {
			if (batchSize > dmlStatementSize || batchSize < 1) throw new IllegalArgumentException("batchSize must be between 1 and 200");
			
			for (int j = 0; j < records.size(); j++) objs[j] = records.get(j);
			saveResults = binding.create(objs, mySessionHeader, null, null, null, null, null, null);

			// Handle the results
		    for (int i = 0; i < saveResults.length; i++) {
		    	
		    	// Determine whether create succeeded or had errors
		    	if (saveResults[i].getSuccess()) {
		    		// No errors, so we will retrieve the id created for this index
		    		records.get(i).setId(saveResults[i].getId());
		    		returnList.add(records.get(i));
		    		logger.log(Level.INFO, "Item " + i + " id" + saveResults[i].getId());
		    		
		    	} else {
		    		// Handle the errors
		    		errorFound = true;
		    		logger.log(Level.SEVERE, "ClientSalesforce.create/Item " + i + " had an error creating.");
		    		logger.log(Level.SEVERE, "ClientSalesforce.create/The error reported was: " + saveResults[i].getErrors()[0].getMessage() + "\n");
		    		errors += saveResults[i].getErrors()[0].getMessage() + "\n"; 
		    	}
		    }
		    if (errorFound) throw new Exception("errors found: " + errors);
			logger.log(Level.INFO, "Query executed sucessfull. Returned Values: " + records.size());
			
		} catch (RemoteException e) {
			logger.log(Level.SEVERE, "ClientSalesforce.create/Error: " + e);
		} catch (InvalidIdFault e) {
			logger.log(Level.SEVERE, "ClientSalesforce.create/Error: " + e);
		} catch (UnexpectedErrorFault e) {
			logger.log(Level.SEVERE, "ClientSalesforce.create/Error: " + e);
		} catch (Exception e) {
			logger.log(Level.SEVERE, "ClientSalesforce.create/Error: " + e);
		}
		return returnList;
	}
	
	/**
	 * Inserts new records into the database when batch size is NOT specified
	 * @param records
	 * @return SaveResult Set of records returned by salesforce (each includes id)
	 * @throws UnexpectedErrorFault
	 * @throws InvalidIdFault
	 * @throws LoginFault
	 * @throws RemoteException
	 * @throws IllegalArgumentException
	 * @throws Exception
	 */
	public static List<SObject> create(List<SObject> records) throws UnexpectedErrorFault, InvalidIdFault,LoginFault,
		RemoteException, IllegalArgumentException, Exception {
		return create(records, dmlStatementSize);
	}

	/**
	 * Updates records into the database when batch size is specified.
	 * Requires new objects list. Do not use the queried list of objects for updating.
	 * @param records
	 * @param batchSize
	 * @return SaveResult Set of records returned by salesforce (each includes id)
	 * @throws UnexpectedErrorFault
	 * @throws InvalidIdFault
	 * @throws LoginFault
	 * @throws RemoteException
	 * @throws IllegalArgumentException
	 * @throws Exception
	 */
	public static List<SObject> update(List<SObject> records, int batchSize) throws UnexpectedErrorFault, InvalidIdFault,LoginFault, 
	RemoteException, IllegalArgumentException, Exception {
		SaveResult[] saveResults;
		boolean errorFound = false;
		String errors = new String();
		SObject[] objs = new SObject[records.size()];
		
		try {
			for(int i=0; i<records.size(); i++) objs[i] = records.get(i);
			// less than the permitted records
			if (batchSize <= dmlStatementSize) saveResults = binding.update(objs, mySessionHeader, null, null, null, null, null, null);
			
			// More than the permitted records
			else {
				saveResults = new SaveResult[objs.length];
				SObject[] thisBatch = null;
				int pos = 0;
				while (pos < objs.length) {			
					int thisBatchSize = Math.min(batchSize,	objs.length - pos);
					if (thisBatch == null ||thisBatch.length != thisBatchSize) thisBatch = new SObject[thisBatchSize];
					logger.info("Records " + pos + "-" + (pos+thisBatchSize));
	
					System.arraycopy(records, pos, thisBatch, 0,thisBatchSize);
					SaveResult [] batchResults = binding.update(thisBatch, mySessionHeader, null, null, null, null, null, null);
					System.arraycopy(batchResults, 0, saveResults,pos, thisBatchSize);
					pos += thisBatchSize;
				}
			}
			
			logger.log(Level.INFO, "Records to update: " + objs[0].toString());
			saveResults = binding.update(objs, mySessionHeader, null, null, null, null, null, null);

			// Handle the results
		    for (int i = 0; i < saveResults.length; i++) {
		    	
		    	// Determine whether create succeeded or had errors
		    	if (saveResults[i].getSuccess()) {
		    		// No errors, so we will retrieve the id created for this index
		    		objs[i].setId(saveResults[i].getId());
		    		logger.log(Level.INFO, "Item " + i + " id" + saveResults[i].getId());
		    		
		    	} else {
		    		// Handle the errors
		    		errorFound = true;
		    		logger.log(Level.SEVERE, "ClientSalesforce.update/Item " + i + " had an error updating.");
		    		logger.log(Level.SEVERE, "ClientSalesforce.update/The error reported was: " + saveResults[i].getErrors()[0].getMessage() + "\n");
		    		errors += saveResults[i].getErrors()[0].getMessage() + "\n"; 
		    	}
		    }
		    if (errorFound) throw new Exception("errors found: " + errors);
			logger.log(Level.INFO, "Query executed sucessfull. Returned Values: " + objs.length);
			for(int i=0; i<objs.length; i++) records.set(i, objs[i]);
			
		} catch (RemoteException e) {
			logger.log(Level.SEVERE, "ClientSalesforce.update/Error: " + e);
		} catch (InvalidIdFault e) {
			logger.log(Level.SEVERE, "ClientSalesforce.update/Error: " + e);
		} catch (UnexpectedErrorFault e) {
			logger.log(Level.SEVERE, "ClientSalesforce.update/Error: " + e);
		} catch (Exception e) {
			logger.log(Level.SEVERE, "ClientSalesforce.update/Error: " + e);
		}
		return records;
	}
	
	public static List<SObject> update(List<SObject> records) throws UnexpectedErrorFault, InvalidIdFault,LoginFault, 
	RemoteException, IllegalArgumentException, Exception {
		return update(records, dmlStatementSize);
	}
	
	/**
	 * Updates a record into the database when batch size is specified
	 * @param records
	 * @param batchSize
	 * @return SaveResult Set of records returned by salesforce (each includes id)
	 * @throws UnexpectedErrorFault
	 * @throws InvalidIdFault
	 * @throws LoginFault
	 * @throws RemoteException
	 * @throws IllegalArgumentException
	 * @throws Exception
	 */
	public static SObject update(SObject record, int batchSize) throws UnexpectedErrorFault, InvalidIdFault,LoginFault, RemoteException, IllegalArgumentException, Exception {
		List<SObject> records = new ArrayList<SObject>();
		records.add(record);
		List<SObject> result = update(records, batchSize);
		
		if (result.size() == 0) return null;
		else return result.get(0);
	}
	
	/**
	 * Updates a record into the database when batch size is not specified
	 * @param records
	 * @param batchSize
	 * @return SaveResult Set of records returned by salesforce (each includes id)
	 * @throws UnexpectedErrorFault
	 * @throws InvalidIdFault
	 * @throws LoginFault
	 * @throws RemoteException
	 * @throws IllegalArgumentException
	 * @throws Exception
	 */
	public static SObject update(SObject record) throws UnexpectedErrorFault, InvalidIdFault,LoginFault, RemoteException, IllegalArgumentException, Exception {
		return update(record, dmlStatementSize);
	}

	/**
	 * Re-executes the login process if it is not active
	 * @throws UnexpectedErrorFault
	 * @throws InvalidIdFault
	 * @throws LoginFault
	 * @throws RemoteException
	 * @throws Exception
	 */
	private static void checkLogin() throws UnexpectedErrorFault,InvalidIdFault, LoginFault, RemoteException, Exception {
		if (sessionId == null || sessionId.length() == 0) login();
	}

	/** This method resets the binding object back to its
	* initial state.
	*/
	private static void resetBindingStub() throws Exception {
		mySessionHeader = new SessionHeader();
		loginResult = new LoginResult();		
		binding = new SforceServiceStub();		
		binding._getServiceClient().getOptions().setTimeOutInMilliSeconds(timeOut);	
		setCompressionOnBinding();
		queryOptions = null;
		userID = null;
	}

	/**
	 * 
	 */
	private static void setCompressionOnBinding() {
		binding._getServiceClient().getOptions().setProperty(HTTPConstants.MC_ACCEPT_GZIP, useCompression);
		binding._getServiceClient().getOptions().setProperty(HTTPConstants.MC_GZIP_REQUEST, useCompression);
	}

	/**
	 * @return the userID
	 */
	public ID getUserID() {
		return userID;
	}

	/**
	 * @return the sessionId
	 */
	public String getSessionId() {
		return sessionId;
	}

}
